<?php
/**
 * Zend Framework
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 *
 * @category   Zend
 * @package    Zend_Filter
 * @copyright  Copyright (c) 2005-2011 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 * @version    $Id: Openssl.php 23775 2011-03-01 17:25:24Z ralph $
 */

/**
 * @see Zend_Filter_Encrypt_Interface
 */
require_once 'Zend/Filter/Encrypt/Interface.php';

/**
 * Encryption adapter for openssl
 *
 * @category   Zend
 * @package    Zend_Filter
 * @copyright  Copyright (c) 2005-2011 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */
class Zend_Filter_Encrypt_Openssl implements Zend_Filter_Encrypt_Interface {
	/**
	 * Definitions for encryption
	 * array(
	 * 'public'   => public keys
	 * 'private'  => private keys
	 * 'envelope' => resulting envelope keys
	 * )
	 */
	protected $_keys = array ('public' => array (), 'private' => array (), 'envelope' => array () );
	
	/**
	 * Internal passphrase
	 *
	 * @var string
	 */
	protected $_passphrase;
	
	/**
	 * Internal compression
	 *
	 * @var array
	 */
	protected $_compression;
	
	/**
	 * Internal create package
	 *
	 * @var boolean
	 */
	protected $_package = false;
	
	/**
	 * Class constructor
	 * Available options
	 * 'public'      => public key
	 * 'private'     => private key
	 * 'envelope'    => envelope key
	 * 'passphrase'  => passphrase
	 * 'compression' => compress value with this compression adapter
	 * 'package'     => pack envelope keys into encrypted string, simplifies decryption
	 *
	 * @param string|array $options Options for this adapter
	 */
	public function __construct($options = array()) {
		if (! extension_loaded ( 'openssl' )) {
			require_once 'Zend/Filter/Exception.php';
			throw new Zend_Filter_Exception ( 'This filter needs the openssl extension' );
		}
		
		if ($options instanceof Zend_Config) {
			$options = $options->toArray ();
		}
		
		if (! is_array ( $options )) {
			$options = array ('public' => $options );
		}
		
		if (array_key_exists ( 'passphrase', $options )) {
			$this->setPassphrase ( $options ['passphrase'] );
			unset ( $options ['passphrase'] );
		}
		
		if (array_key_exists ( 'compression', $options )) {
			$this->setCompression ( $options ['compression'] );
			unset ( $options ['compress'] );
		}
		
		if (array_key_exists ( 'package', $options )) {
			$this->setPackage ( $options ['package'] );
			unset ( $options ['package'] );
		}
		
		$this->_setKeys ( $options );
	}
	
	/**
	 * Sets the encryption keys
	 *
	 * @param  string|array $keys Key with type association
	 * @return Zend_Filter_Encrypt_Openssl
	 */
	protected function _setKeys($keys) {
		if (! is_array ( $keys )) {
			require_once 'Zend/Filter/Exception.php';
			throw new Zend_Filter_Exception ( 'Invalid options argument provided to filter' );
		}
		
		foreach ( $keys as $type => $key ) {
			if (is_file ( $key ) and is_readable ( $key )) {
				$file = fopen ( $key, 'r' );
				$cert = fread ( $file, 8192 );
				fclose ( $file );
			} else {
				$cert = $key;
				$key = count ( $this->_keys [$type] );
			}
			
			switch ($type) {
				case 'public' :
					$test = openssl_pkey_get_public ( $cert );
					if ($test === false) {
						require_once 'Zend/Filter/Exception.php';
						throw new Zend_Filter_Exception ( "Public key '{$cert}' not valid" );
					}
					
					openssl_free_key ( $test );
					$this->_keys ['public'] [$key] = $cert;
					break;
				case 'private' :
					$test = openssl_pkey_get_private ( $cert, $this->_passphrase );
					if ($test === false) {
						require_once 'Zend/Filter/Exception.php';
						throw new Zend_Filter_Exception ( "Private key '{$cert}' not valid" );
					}
					
					openssl_free_key ( $test );
					$this->_keys ['private'] [$key] = $cert;
					break;
				case 'envelope' :
					$this->_keys ['envelope'] [$key] = $cert;
					break;
				default :
					break;
			}
		}
		
		return $this;
	}
	
	/**
	 * Returns all public keys
	 *
	 * @return array
	 */
	public function getPublicKey() {
		$key = $this->_keys ['public'];
		return $key;
	}
	
	/**
	 * Sets public keys
	 *
	 * @param  string|array $key Public keys
	 * @return Zend_Filter_Encrypt_Openssl
	 */
	public function setPublicKey($key) {
		if (is_array ( $key )) {
			foreach ( $key as $type => $option ) {
				if ($type !== 'public') {
					$key ['public'] = $option;
					unset ( $key [$type] );
				}
			}
		} else {
			$key = array ('public' => $key );
		}
		
		return $this->_setKeys ( $key );
	}
	
	/**
	 * Returns all private keys
	 *
	 * @return array
	 */
	public function getPrivateKey() {
		$key = $this->_keys ['private'];
		return $key;
	}
	
	/**
	 * Sets private keys
	 *
	 * @param  string $key Private key
	 * @param  string $passphrase
	 * @return Zend_Filter_Encrypt_Openssl
	 */
	public function setPrivateKey($key, $passphrase = null) {
		if (is_array ( $key )) {
			foreach ( $key as $type => $option ) {
				if ($type !== 'private') {
					$key ['private'] = $option;
					unset ( $key [$type] );
				}
			}
		} else {
			$key = array ('private' => $key );
		}
		
		if ($passphrase !== null) {
			$this->setPassphrase ( $passphrase );
		}
		
		return $this->_setKeys ( $key );
	}
	
	/**
	 * Returns all envelope keys
	 *
	 * @return array
	 */
	public function getEnvelopeKey() {
		$key = $this->_keys ['envelope'];
		return $key;
	}
	
	/**
	 * Sets envelope keys
	 *
	 * @param  string|array $options Envelope keys
	 * @return Zend_Filter_Encrypt_Openssl
	 */
	public function setEnvelopeKey($key) {
		if (is_array ( $key )) {
			foreach ( $key as $type => $option ) {
				if ($type !== 'envelope') {
					$key ['envelope'] = $option;
					unset ( $key [$type] );
				}
			}
		} else {
			$key = array ('envelope' => $key );
		}
		
		return $this->_setKeys ( $key );
	}
	
	/**
	 * Returns the passphrase
	 *
	 * @return string
	 */
	public function getPassphrase() {
		return $this->_passphrase;
	}
	
	/**
	 * Sets a new passphrase
	 *
	 * @param string $passphrase
	 * @return Zend_Filter_Encrypt_Openssl
	 */
	public function setPassphrase($passphrase) {
		$this->_passphrase = $passphrase;
		return $this;
	}
	
	/**
	 * Returns the compression
	 *
	 * @return array
	 */
	public function getCompression() {
		return $this->_compression;
	}
	
	/**
	 * Sets a internal compression for values to encrypt
	 *
	 * @param string|array $compression
	 * @return Zend_Filter_Encrypt_Openssl
	 */
	public function setCompression($compression) {
		if (is_string ( $this->_compression )) {
			$compression = array ('adapter' => $compression );
		}
		
		$this->_compression = $compression;
		return $this;
	}
	
	/**
	 * Returns if header should be packaged
	 *
	 * @return boolean
	 */
	public function getPackage() {
		return $this->_package;
	}
	
	/**
	 * Sets if the envelope keys should be included in the encrypted value
	 *
	 * @param boolean $package
	 * @return Zend_Filter_Encrypt_Openssl
	 */
	public function setPackage($package) {
		$this->_package = ( boolean ) $package;
		return $this;
	}
	
	/**
	 * Encrypts $value with the defined settings
	 * Note that you also need the "encrypted" keys to be able to decrypt
	 *
	 * @param  string $value Content to encrypt
	 * @return string The encrypted content
	 * @throws Zend_Filter_Exception
	 */
	public function encrypt($value) {
		$encrypted = array ();
		$encryptedkeys = array ();
		
		if (count ( $this->_keys ['public'] ) == 0) {
			require_once 'Zend/Filter/Exception.php';
			throw new Zend_Filter_Exception ( 'Openssl can not encrypt without public keys' );
		}
		
		$keys = array ();
		$fingerprints = array ();
		$count = - 1;
		foreach ( $this->_keys ['public'] as $key => $cert ) {
			$keys [$key] = openssl_pkey_get_public ( $cert );
			if ($this->_package) {
				$details = openssl_pkey_get_details ( $keys [$key] );
				if ($details === false) {
					$details = array ('key' => 'ZendFramework' );
				}
				
				++ $count;
				$fingerprints [$count] = md5 ( $details ['key'] );
			}
		}
		
		// compress prior to encryption
		if (! empty ( $this->_compression )) {
			require_once 'Zend/Filter/Compress.php';
			$compress = new Zend_Filter_Compress ( $this->_compression );
			$value = $compress->filter ( $value );
		}
		
		$crypt = openssl_seal ( $value, $encrypted, $encryptedkeys, $keys );
		foreach ( $keys as $key ) {
			openssl_free_key ( $key );
		}
		
		if ($crypt === false) {
			require_once 'Zend/Filter/Exception.php';
			throw new Zend_Filter_Exception ( 'Openssl was not able to encrypt your content with the given options' );
		}
		
		$this->_keys ['envelope'] = $encryptedkeys;
		
		// Pack data and envelope keys into single string
		if ($this->_package) {
			$header = pack ( 'n', count ( $this->_keys ['envelope'] ) );
			foreach ( $this->_keys ['envelope'] as $key => $envKey ) {
				$header .= pack ( 'H32n', $fingerprints [$key], strlen ( $envKey ) ) . $envKey;
			}
			
			$encrypted = $header . $encrypted;
		}
		
		return $encrypted;
	}
	
	/**
	 * Defined by Zend_Filter_Interface
	 *
	 * Decrypts $value with the defined settings
	 *
	 * @param  string $value Content to decrypt
	 * @return string The decrypted content
	 * @throws Zend_Filter_Exception
	 */
	public function decrypt($value) {
		$decrypted = "";
		$envelope = current ( $this->getEnvelopeKey () );
		
		if (count ( $this->_keys ['private'] ) !== 1) {
			require_once 'Zend/Filter/Exception.php';
			throw new Zend_Filter_Exception ( 'Please give a private key for decryption with Openssl' );
		}
		
		if (! $this->_package && empty ( $envelope )) {
			require_once 'Zend/Filter/Exception.php';
			throw new Zend_Filter_Exception ( 'Please give a envelope key for decryption with Openssl' );
		}
		
		foreach ( $this->_keys ['private'] as $key => $cert ) {
			$keys = openssl_pkey_get_private ( $cert, $this->getPassphrase () );
		}
		
		if ($this->_package) {
			$details = openssl_pkey_get_details ( $keys );
			if ($details !== false) {
				$fingerprint = md5 ( $details ['key'] );
			} else {
				$fingerprint = md5 ( "ZendFramework" );
			}
			
			$count = unpack ( 'ncount', $value );
			$count = $count ['count'];
			$length = 2;
			for($i = $count; $i > 0; -- $i) {
				$header = unpack ( 'H32print/nsize', substr ( $value, $length, 18 ) );
				$length += 18;
				if ($header ['print'] == $fingerprint) {
					$envelope = substr ( $value, $length, $header ['size'] );
				}
				
				$length += $header ['size'];
			}
			
			// remainder of string is the value to decrypt
			$value = substr ( $value, $length );
		}
		
		$crypt = openssl_open ( $value, $decrypted, $envelope, $keys );
		openssl_free_key ( $keys );
		
		if ($crypt === false) {
			require_once 'Zend/Filter/Exception.php';
			throw new Zend_Filter_Exception ( 'Openssl was not able to decrypt you content with the given options' );
		}
		
		// decompress after decryption
		if (! empty ( $this->_compression )) {
			require_once 'Zend/Filter/Decompress.php';
			$decompress = new Zend_Filter_Decompress ( $this->_compression );
			$decrypted = $decompress->filter ( $decrypted );
		}
		
		return $decrypted;
	}
	
	/**
	 * Returns the adapter name
	 *
	 * @return string
	 */
	public function toString() {
		return 'Openssl';
	}
}
